package test;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.Properties;

import org.apache.lucene.analysis.SimpleAnalyzer;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.IndexWriterConfig.OpenMode;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.apache.lucene.store.LockFactory;
import org.apache.lucene.store.MMapDirectory;
import org.apache.lucene.store.NIOFSDirectory;
import org.apache.lucene.store.NativeFSLockFactory;
import org.apache.lucene.store.NoLockFactory;
import org.apache.lucene.store.SimpleFSDirectory;
import org.apache.lucene.store.SimpleFSLockFactory;
import org.apache.lucene.store.SingleInstanceLockFactory;
import org.apache.lucene.util.Version;
import org.hibernate.annotations.common.util.StringHelper;
import org.hibernate.search.Environment;
import org.hibernate.search.SearchException;
import org.hibernate.search.indexes.impl.DirectoryBasedIndexManager;
import org.hibernate.search.spi.BuildContext;
import org.hibernate.search.store.DirectoryProvider;
import org.hibernate.search.store.LockFactoryProvider;
import org.hibernate.search.store.impl.DirectoryProviderHelper;
import org.hibernate.search.util.impl.ClassLoaderHelper;
import org.hibernate.search.util.logging.impl.Log;
import org.hibernate.search.util.logging.impl.LoggerFactory;

/**
 * 
 * @author Gao Youbo
 * @since 2013年7月5日
 */
public class FSDirectoryProvider implements DirectoryProvider<FSDirectory> {

    private static final Log log = LoggerFactory.make();

    private FSDirectory directory;
    private String indexName;

    @Override
    public void initialize(String directoryProviderName, Properties properties, BuildContext context) {
        // on "manual" indexing skip read-write check on index directory
        boolean manual = context.getIndexingStrategy().equals("manual");
        File indexDir = DirectoryProviderHelper.getVerifiedIndexDir(directoryProviderName, properties, !manual);
        try {
            indexName = indexDir.getCanonicalPath();
            // this is cheap so it's not done in start()
            directory = DirectoryProviderHelper.createFSIndex(indexDir, properties);
            // directory = createFSIndex(indexDir, properties);
        } catch (IOException e) {
            throw new SearchException("Unable to initialize index: " + directoryProviderName, e);
        }
    }

    @Override
    public void start(DirectoryBasedIndexManager indexManager) {
        // all the process is done in initialize
    }

    public void stop() {
        try {
            directory.close();
        } catch (Exception e) {
            log.unableToCloseLuceneDirectory(directory.getDirectory(), e);
        }
    }

    public FSDirectory getDirectory() {
        return directory;
    }

    @Override
    public boolean equals(Object obj) {
        // this code is actually broken since the value change after initialize
        // call
        // but from a practical POV this is fine since we only call this method
        // after initialize call
        if (obj == this) {
            return true;
        }
        if (obj == null || !(obj instanceof FSDirectoryProvider)) {
            return false;
        }
        return indexName.equals(((FSDirectoryProvider) obj).indexName);
    }

    @Override
    public int hashCode() {
        // this code is actually broken since the value change after initialize
        // call
        // but from a practical POV this is fine since we only call this method
        // after initialize call
        int hash = 11;
        return 37 * hash + indexName.hashCode();
    }

    // //////////////////////////////////////////////////

    private static final String LOCKING_STRATEGY_PROP_NAME = "locking_strategy";
    private static final String FS_DIRECTORY_TYPE_PROP_NAME = "filesystem_access_type";

    /**
     * Creates an FSDirectory in provided directory and initializes an index if
     * not already existing.
     * 
     * @param indexDir
     *            the directory where to write a new index
     * @param properties
     *            the configuration properties
     * @return the created {@code FSDirectory} instance
     * @throws java.io.IOException
     *             if an error
     */
    public static FSDirectory createFSIndex(File indexDir, Properties properties) throws IOException {
        LockFactory lockFactory = createLockFactory(indexDir, properties);
        FSDirectoryType fsDirectoryType = FSDirectoryType.getType(properties);
        FSDirectory fsDirectory = fsDirectoryType.getDirectory(indexDir, null);

        // must use the setter (instead of using the constructor) to set the
        // lockFactory, or Lucene will
        // throw an exception if it's different than a previous setting.
        fsDirectory.setLockFactory(lockFactory);
        log.debugf("Initialize index: '%s'", indexDir.getAbsolutePath());
        initializeIndexIfNeeded(fsDirectory);
        return fsDirectory;
    }

    /**
     * Initialize the Lucene Directory if it isn't already.
     * 
     * @param directory
     *            the Directory to initialize
     * @throws SearchException
     *             in case of lock acquisition timeouts, IOException, or if a
     *             corrupt index is found
     */
    public static void initializeIndexIfNeeded(Directory directory) {
        // version doesn't really matter as we won't use the Analyzer
        Version version = Environment.DEFAULT_LUCENE_MATCH_VERSION;
        SimpleAnalyzer analyzer = new SimpleAnalyzer(version);
        try {
            if (!IndexReader.indexExists(directory)) {
                IndexWriterConfig iwriterConfig = new IndexWriterConfig(version, analyzer).setOpenMode(OpenMode.CREATE);
                IndexWriter iw = new IndexWriter(directory, iwriterConfig);
                iw.close();
            }
        } catch (IOException e) {
            throw new SearchException("Could not initialize index", e);
        } finally {
            analyzer.close();
        }
    }

    public static LockFactory createLockFactory(File indexDir, Properties dirConfiguration) {
        // For FS-based indexes default to "native", default to "single"
        // otherwise.
        String defaultStrategy = indexDir == null ? "single" : "native";
        String lockFactoryName = dirConfiguration.getProperty(LOCKING_STRATEGY_PROP_NAME, defaultStrategy);
        if ("simple".equals(lockFactoryName)) {
            if (indexDir == null) {
                throw new SearchException("To use \"simple\" as a LockFactory strategy an indexBase path must be set");
            }
            try {
                return new SimpleFSLockFactory(indexDir);
            } catch (IOException e) {
                throw new SearchException("Could not initialize SimpleFSLockFactory", e);
            }
        } else if ("native".equals(lockFactoryName)) {
            if (indexDir == null) {
                throw new SearchException("To use \"native\" as a LockFactory strategy an indexBase path must be set");
            }
            try {
                return new NativeFSLockFactory(indexDir);
            } catch (IOException e) {
                throw new SearchException("Could not initialize NativeFSLockFactory", e);
            }
        } else if ("single".equals(lockFactoryName)) {
            return new SingleInstanceLockFactory();
        } else if ("none".equals(lockFactoryName)) {
            return NoLockFactory.getNoLockFactory();
        } else {
            LockFactoryProvider lockFactoryFactory = ClassLoaderHelper.instanceFromName(LockFactoryProvider.class, lockFactoryName, DirectoryProviderHelper.class, LOCKING_STRATEGY_PROP_NAME);
            return lockFactoryFactory.createLockFactory(indexDir, dirConfiguration);
        }
    }

    private enum FSDirectoryType {
        AUTO(null), SIMPLE(SimpleFSDirectory.class), NIO(NIOFSDirectory.class), MMAP(MMapDirectory.class);

        private Class<?> fsDirectoryClass;

        FSDirectoryType(Class<?> fsDirectoryClass) {
            this.fsDirectoryClass = fsDirectoryClass;
        }

        public FSDirectory getDirectory(File indexDir, LockFactory factory) throws IOException {
            FSDirectory directory;
            if (fsDirectoryClass == null) {
                directory = FSDirectory.open(indexDir, factory);
            } else {
                try {
                    Constructor constructor = fsDirectoryClass.getConstructor(File.class, LockFactory.class);
                    directory = (FSDirectory) constructor.newInstance(indexDir, factory);
                } catch (NoSuchMethodException e) {
                    throw new SearchException("Unable to find appropriate FSDirectory constructor", e);
                } catch (InstantiationException e) {
                    throw new SearchException("Unable to instantiate FSDirectory class " + fsDirectoryClass.getName(), e);
                } catch (IllegalAccessException e) {
                    throw new SearchException("Unable to instantiate FSDirectory class " + fsDirectoryClass.getName(), e);
                } catch (InvocationTargetException e) {
                    throw new SearchException("Unable to instantiate FSDirectory class " + fsDirectoryClass.getName(), e);
                }
            }
            return directory;
        }

        public static FSDirectoryType getType(Properties properties) {
            FSDirectoryType fsDirectoryType;
            String fsDirectoryTypeValue = properties.getProperty(FS_DIRECTORY_TYPE_PROP_NAME);
            if (StringHelper.isNotEmpty(fsDirectoryTypeValue)) {
                try {
                    fsDirectoryType = Enum.valueOf(FSDirectoryType.class, fsDirectoryTypeValue.toUpperCase());
                } catch (IllegalArgumentException e) {
                    throw new SearchException("Invalid option value for " + FS_DIRECTORY_TYPE_PROP_NAME + ": " + fsDirectoryTypeValue);
                }
            } else {
                fsDirectoryType = AUTO;
            }
            return fsDirectoryType;
        }
    }
}